/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: AVTransport.java,v $
 *
 */

package com.cidero.upnp;

import java.util.logging.Logger;
import java.util.logging.Level;

import org.cybergarage.upnp.*;
import org.cybergarage.upnp.device.InvalidDescriptionException;

import com.cidero.upnp.AbstractService;

/**
 *  AVTransport service abstract base class. Provides default implementations
 *  for some actions where possible, but mostly just defines dumb methods
 *  that just return 'false' (action not supported).
 *
 *  Notes:
 *
 *  Method header documentation in this class is intended to be the 
 *  'master' copy.  Derived classes should just include
 *  implemention-specific details in their method headers
 */
public class AVTransport extends AbstractService
{
  private static Logger logger = Logger.getLogger("com.cidero.upnp");

  // TransportState values
  public final static String STATE_STOPPED = "STOPPED";
  public final static String STATE_PLAYING = "PLAYING";
  public final static String STATE_TRANS   = "TRANSITIONING";
  public final static String STATE_PAUSED_PLAYBACK = "PAUSED_PLAYBACK";
  public final static String STATE_PAUSED_RECORDING = "PAUSED_RECORDING";
  public final static String STATE_RECORDING = "RECORDING";
  public final static String STATE_NO_MEDIA = "NO_MEDIA_PRESENT";

  // TransportStatus values
  public final static String STATUS_OK = "OK";
  public final static String STATUS_ERROR_OCCURRED = "ERROR_OCCURRED";
  // Note: DLINK uses vendor-specific value of "STATUS_MEDIA_END" in
  // some circumstances (after display of a picture)

  // PlayMode values
  public final static String PLAY_MODE_NORMAL  = "NORMAL";
  public final static String PLAY_MODE_SHUFFLE = "SHUFFLE";
  public final static String PLAY_MODE_REPEAT_ONE = "REPEAT_ONE";
  public final static String PLAY_MODE_REPEAT_ALL = "REPEAT_ALL";
  public final static String PLAY_MODE_RANDOM = "RANDOM";
  public final static String PLAY_MODE_DIRECT_1 = "DIRECT_1";
  public final static String PLAY_MODE_INTRO = "INTRO";

  public final static String NOT_IMPLEMENTED = "NOT_IMPLEMENTED";
  // Some variables use the biggest value of an i4 to signify 'NOT_IMPLEMENTED'
  public final static String NOT_IMPLEMENTED_I4 = "2147483647";

  public final static String URN = "urn:schemas-upnp-org:service:AVTransport:1";

  // Service-specific error codes (basic UPnP error codes handled by 
  // org.cybergarage.upnp.UPnPStatus class)

  public static final int TRANSITION_NOT_AVAILABLE = 701;
  public static final int NO_CONTENTS = 702;
  public static final int READ_ERROR = 703;
  public static final int FORMAT_NOT_SUPPORTED_FOR_PLAYBACK = 704;
  public static final int TRANSPORT_LOCKED = 705;
  public static final int WRITE_ERROR = 706;
  public static final int MEDIA_NOT_WRITABLE = 707;
  public static final int FORMAT_NOT_SUPPORTED_FOR_RECORDING = 708;
  public static final int MEDIA_FULL = 709;
  public static final int SEEK_MODE_NOT_SUPPORTED = 710;
  public static final int ILLEGAL_SEEK_TARGET = 711;
  public static final int BAD_METADATA = 712;
  public static final int RECORD_QUALITY_NOT_SUPPORTED = 713;
  public static final int ILLEGAL_MIME_TYPE = 714;
  public static final int CONTENT_BUSY = 715;
  public static final int RESOURCE_NOT_FOUND = 716;
  public static final int PLAY_SPEED_NOT_SUPPORTED = 717;
  public static final int INVALID_INSTANCE_ID = 718;
  
  long  startTimeMillis;   // local system time at start of playback

  /**
   * Constructor
   */
  public AVTransport( Device device )
    throws InvalidDescriptionException
  {
    super( device );
    logger.fine("Entered AVTransport base class constructor");

    logger.fine("Leaving AVTransport constructor");
  }
	
  /**
   * No argument constructor. Useful when one wants to instantiate an
   * object to provide access to errorToString() method (among others)
   * TODO: There may be better way to deal with this...(OJN)
   */
  public AVTransport()
  {
  }

  public String getServiceType()
  {
    return "urn:schemas-upnp-org:service:AVTransport:1";
  }

  /**
   *  Initialize all required state variables to reasonable values 
   *  Optional device-specific state variables are initialized in the 
   *  derived class instances
   */
  public void initializeStateVariables()
  {
    setStateVariable("TransportState", STATE_STOPPED );
    setStateVariable("TransportStatus", "OK" );
    setStateVariable("PlaybackStorageMedium", "NONE" );
    setStateVariable("RecordStorageMedium", NOT_IMPLEMENTED );
    setStateVariable("PossiblePlaybackStorageMedia", "" );
    setStateVariable("PossibleRecordStorageMedia", NOT_IMPLEMENTED );
    setStateVariable("CurrentPlayMode", PLAY_MODE_NORMAL ); 
    setStateVariable("TransportPlaySpeed", "1" );
    setStateVariable("RecordMediumWriteStatus", NOT_IMPLEMENTED );
    setStateVariable("CurrentRecordQualityMode", NOT_IMPLEMENTED );
    setStateVariable("PossibleRecordQualityModes", "" );
    setStateVariable("NumberOfTracks", "0" );
    setStateVariable("CurrentTrack", "0" );
    setStateVariable("CurrentTrackDuration", "00:00:00" );
    setStateVariable("CurrentMediaDuration", "00:00:00" );
    setStateVariable("CurrentTrackMetaData", "" );
    setStateVariable("CurrentTrackURI", "" );
    setStateVariable("AVTransportURI", "" );
    setStateVariable("AVTransportURIMetaData", "" );
    setStateVariable("NextAVTransportURI", NOT_IMPLEMENTED );
    setStateVariable("NextAVTransportURIMetaData", NOT_IMPLEMENTED );
    setStateVariable("RelativeTimePosition", "00:00:00" );
    setStateVariable("AbsoluteTimePosition", NOT_IMPLEMENTED );
    setStateVariable("RelativeCounterPosition", NOT_IMPLEMENTED_I4 );
    setStateVariable("AbsoluteCounterPosition", NOT_IMPLEMENTED_I4 );

    // Note: required LastChange state variable is (ugly) special case - 
    // don't need to initialize it here.
    //setStateVariable("LastChange", "" );

  }

  /** 
   * Convenience routine for AVTransport service 'LastChange' eventing
   * mechanism.  Sets state variable and sends out a LastChange event
   * in one shot.  The same routine exists for the RenderingControl
   * service (don't want to put it in base class since it is service-specific)
   *
   * This method synchronized to be safe since it is often invoked from two
   * threads - the main action processing thread and and asynchronous
   * device monitoring thread.
   *
   * @param varName   State variable name (e.g. 'Volume')
   * @param value     New value for state variable
   */
  public synchronized boolean updateStateVariable( String varName,
                                                   String value )
  {
    //System.out.println("!!!!!!!!!!!!!!!!! AV UPDATE STATE VAR !!!!!!!!\n");
    //System.out.println("!!!!!! Var, val = " + varName + " " + value );
    //System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");

    setStateVariable( varName, value );
    getService().updateLastChangeStateVariable();  // evented - triggers send
    return true;
  }
      
  /**
   * Get information associated with the current media of a transport 
   * instance.  
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      NrTracks              Number of tracks
   *      MediaDuration         Media duration as HH:MM:SS (e.g. "10:00" )
   *      CurrentURI            Current URI 
   *      CurrentURIMetaData    DIDL-Lite metadata for URI
   *      NextURI             
   *      NextURIMetaData
   *      PlayMedium          
   *      RecordMedium
   *      WriteStatus
   *
   *
   *  NMPR Compliance Notes:
   *    Required Action: Yes
   *
   */
  public boolean actionGetMediaInfo( Action action )
  {
    // Example of getting action argument value
    //String instanceID = action.getArgumentValue("InstanceID");

    // Example of setting action argument value
    //action.setArgumentValue("NrTracks", "1" );

    // Related state variable is 'NumberOfTracks'
    action.setArgumentValueFromRelatedStateVariable("NrTracks");

    // Related state variable is 'CurrentMediaDuration'
    action.setArgumentValueFromRelatedStateVariable("MediaDuration");

    // Related state variable is 'AVTransportURI'
    action.setArgumentValueFromRelatedStateVariable("CurrentURI");

    // Related state variable is 'AVTransportURIMetaData'
    action.setArgumentValueFromRelatedStateVariable("CurrentURIMetaData");

    // Related state variable is 'NextAVTransportURI'
    action.setArgumentValueFromRelatedStateVariable("NextURI");

    // Related state variable is 'NextAVTransportURIMetaData'
    action.setArgumentValueFromRelatedStateVariable("NextURIMetaData");

    // Related state variable is 'PlaybackStorageMedium'
    action.setArgumentValueFromRelatedStateVariable("PlayMedium");

    // Related state variable is 'RecordStorageMedium'
    action.setArgumentValueFromRelatedStateVariable("RecordMedium");

    // Related state variable is 'RecordMediumWriteStatus'
    action.setArgumentValueFromRelatedStateVariable("WriteStatus");

    return true;
  }

  /**
   * Get information associated with the specified transport instance
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      CurrentTransportState        See STATE_<XXX> constants above
   *      CurrentTransportStatus       
   *      CurrentSpeed       
   *
   *
   *  NMPR Compliance Notes:
   *    Required Action: Yes
   *
   */
  public boolean actionGetTransportInfo( Action action )
  {
    // Related state variable is 'TransportState'
    action.setArgumentValueFromRelatedStateVariable("CurrentTransportState");

    // Related state variable is 'TransportStatus'
    action.setArgumentValueFromRelatedStateVariable("CurrentTransportStatus");

    // Related state variable is 'TransportPlaySpeed'
    action.setArgumentValueFromRelatedStateVariable("CurrentSpeed");

    return true;
  }

  /**
   * Get information associated with the current position of a transport 
   * instance. This method is commonly used by control points to see
   * where renderers are in a playback. 
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      Track
   *      TrackDuration
   *      TrackMetaData
   *      TrackURI
   *      RelTime
   *      AbsTime
   *      RelCount
   *      AbsCount
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   *
   */
  public boolean actionGetPositionInfo( Action action )
  {
    String instanceID = action.getArgumentValue("InstanceID");

    logger.finest("GetPositionInfo: Entered, instanceId = " +
                  instanceID );

    // Related state variable is 'CurrentTrack'
    action.setArgumentValueFromRelatedStateVariable("Track");

    // Related state variable is 'CurrentTrackDuration'
    action.setArgumentValueFromRelatedStateVariable("TrackDuration");

    // Related state variable is 'CurrentTrackMetaData'
    action.setArgumentValueFromRelatedStateVariable("TrackMetaData");

    // Related state variable is 'CurrentTrackURI'
    action.setArgumentValueFromRelatedStateVariable("TrackURI");

    // Related state variable is 'RelativeTimePosition'
    action.setArgumentValueFromRelatedStateVariable("RelTime");

    // Related state variable is 'AbsoluteTimePosition'
    action.setArgumentValueFromRelatedStateVariable("AbsTime");

    // Related state variable is 'RelativeCounterPosition'
    action.setArgumentValueFromRelatedStateVariable("RelCount");

    // Related state variable is 'AbsoluteCounterPosition'
    action.setArgumentValueFromRelatedStateVariable("AbsCount");

    return true;
  }
  
  /**
   * Get device capabilities associated with the transport 
   * instance 
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      PlayMedia              "UNKNOWN" ?  (TODO - Check)
   *      RecMedia               "UNKNOWN" ?  (TODO - Check)
   *      RecQualityModes        "NOT_IMPLEMENTED" (TODO - Check)
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes (but Intel tools don't seem to invoke this yet)
   */
  public boolean actionGetDeviceCapabilities( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("GetDeviceCapabilities: Entered, instanceID = " + id );

    // Related state variable is 'PossiblePlaybackStorageMedia'
    action.setArgumentValueFromRelatedStateVariable("PlayMedia");

    // Related state variable is 'PossibleRecordStorageMedia'
    action.setArgumentValueFromRelatedStateVariable("RecMedia");

    // Related state variable is 'PossibleRecordQualityModes'
    action.setArgumentValueFromRelatedStateVariable("RecQualityModes");

    return true;
  }

  /**
   * Set the URI to use for subsequent play actions.  
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      CurrentURI
   *      CurrentURIMetaData    DIDL-Lite MetaData for URI
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   *
   */
  public boolean actionSetAVTransportURI( Action action )
  {
    String currentURI = action.getArgumentValue("CurrentURI");

    if (logger.isLoggable(Level.FINE) ) 
    {
      String instanceID = action.getArgumentValue("InstanceID");
      String metadata = action.getArgumentValue("CurrentURIMetaData");
      logger.fine("SetAVTransportURI: Id = " + instanceID +
                  "  uri = " + currentURI );
      logger.fine("Metadata: " + metadata );
    }
    
    // Set state variables that are associated with arguments
    // store currentURI for later use in play/pause/stop methods

    // Related variable is 'AVTransportURI'...
    if( !action.setRelatedStateVariableFromArgument("CurrentURI") )
      return false;

    // Related variable is 'AVTransportURIMetaData'...
    if( !action.setRelatedStateVariableFromArgument("CurrentURIMetaData") )
      return false;

    // Assume network playback if non-empty URI specified
    if( currentURI != null && !currentURI.equals("") )
      setStateVariable("PlaybackStorageMedium", "NETWORK" );
    else
      setStateVariable("PlaybackStorageMedium", "NONE" );

    return true;
  }

  /**
   * Set the next URI to use after playback of the current URI is
   * completed
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      NextURI             Next URI to play after current one is done 
   *      NextURIMetaData     DIDL-Lite MetaData for URI
   *
   *    Action output arguments:
   *      None
   *
   * Notes: 
   *
   *  NMPR rules say don't implement this, but seems like there may be a
   *  need for it when implementing flexible jukebox apps (to keep music
   *  flowing with no pauses)
   *
   *
   * NMPR Compliance Notes:
   *   Required Action: No - compliant device shouldn't advertise this action
   *
   */
  public boolean actionSetNextAVTransportURI( Action action )
  {
    String nextURI = action.getArgumentValue("NextURI");
    String nextURIMetaData = action.getArgumentValue("NextURIMetaData");

    if (logger.isLoggable(Level.FINE) ) 
      logger.fine("SetNextAVTransportURI: Entered - NextURI: " +
                  nextURI + "\nMetaData: " + nextURIMetaData );

    // Related variable is 'NextAVTransportURI'...
    if( !action.setRelatedStateVariableFromArgument("NextURI") )
      return false;

    // Related variable is 'NextAVTransportURIMetaData'...
    if( !action.setRelatedStateVariableFromArgument("NextURIMetaData") )
      return false;

    return true;
  }

  /**
   * Start/Restart playback of the current session.
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      Speed               Requested playback speed (0.0-1.0? TODO: CHECK)
   *
   *    Action output arguments:
   *      None
   *
   * Notes: Implementation is highly device-specific, so no default 
   *        provided here 
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   */
  public boolean actionPlay( Action action )
  {
    return false;
  }

  /**
   * Pause playback of the current session.
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   */
  public boolean actionPause( Action action )
  {
    return false;
  }

  /**
   * Stop playback/recording
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   */
  public boolean actionStop( Action action )
  {
    return false;
  }
  
  /**
   * Seek 
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      Unit             Seek mode  TODO: Better description
   *      Target           Seek target TODO: Better description
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   */
  public boolean actionSeek( Action action )
  {
    return false;
  }

  /**
   * Next
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   */
  public boolean actionNext( Action action )
  {
    return false;
  }

  /**
   * Previous
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: No
   */
  public boolean actionPrevious( Action action )
  {
    return false;
  }

  /**
   * Start recording
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: No
   */
  public boolean actionRecord( Action action )
  {
    return false;
  }

  /**
   * Set the play mode. 
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      NewPlayMode      NORMAL, SHUFFLE, REPEAT_ONE, REPEAT_ALL, RANDOM,
   *                       DIRECT_1, and INTRO
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: No
   */
  public boolean actionSetPlayMode( Action action )
  {
    // Related variable is 'CurrentPlayMode'...
    //if( !action.setRelatedStateVariableFromArgument("NewPlayMode") )
    //    return false;

    // PlayMode is one of the variables evented via the 'LastChange'
    // mechanism - handled by updateStateVariable()

    String newPlayMode = action.getArgumentValue("NewPlayMode");
    return updateStateVariable( "CurrentPlayMode", newPlayMode );
  }

  /**
   * Set the recording quality mode. 
   *
   * @param  action  UPNP Action object
   *
   *    Action input arguments:
   *      InstanceID 
   *      NewRecordQualityMode   One of 0:EP, 1:LP, 2:SP, 0:BASIC, 1:MEDIUM,
   *                             2:HIGH, or NOT_IMPLEMENTED
   *
   *    Action output arguments:
   *      None
   *
   * NMPR Compliance Notes:
   *   Required Action: No
   */
  public boolean actionSetRecordQualityMode( Action action )
  {
    // Related variable is 'CurrentRecordQualityMode'...
    if( !action.setRelatedStateVariableFromArgument("NewRecordQualityMode") )
      return false;
    
    return true;
  }
  
  /**
   * Get device transport settings
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      PlayMode         NORMAL, SHUFFLE, REPEAT_ONE, REPEAT_ALL, RANDOM,
   *                       DIRECT_1, and INTRO
   *      RecQualityMode   One of 0:EP, 1:LP, 2:SP, 0:BASIC, 1:MEDIUM,
   *                       2:HIGH, and NOT_IMPLEMENTED
   *
   * NMPR Compliance Notes:
   *   Required Action: Yes
   *
   */
  public boolean actionGetTransportSettings( Action action )
  {
    String id = action.getArgumentValue("InstanceID");

    logger.fine("GetTransportSettings: Entered, id = " + id );
    
    // Related variable is 'CurrentPlayMode'...
    action.setArgumentValueFromRelatedStateVariable("PlayMode");
    action.setArgumentValueFromRelatedStateVariable("RecQualityMode");

    return false;  // TODO - enable this action
  }

  /**
   * Get current transport actions
   *
   * @param  action  UPnP action object
   *
   *    Action input arguments:
   *      InstanceID 
   *
   *    Action output arguments:
   *      Actions          ??? TODO:
   *                       DIRECT_1, and INTRO
   *
   * NMPR Compliance Notes:
   *   Required Action: No
   *
   */
  public boolean actionGetCurrentTransportActions( Action action )
  {
    logger.fine("Entered" );

    // Related variable is 'CurrentTransportActions'...
    action.setArgumentValueFromRelatedStateVariable("Actions");

    return true;
  }


  public String errorToString(int code)
  {
    return codeToString( code );
  }

  public static final String codeToString(int code)
  {
    switch (code) 
    {
      case INVALID_INSTANCE_ID: return "Invalid instance id";
      case TRANSITION_NOT_AVAILABLE: return "Transition not available";
      case NO_CONTENTS: return "No contents";
      case READ_ERROR: return "Read error";
      case FORMAT_NOT_SUPPORTED_FOR_PLAYBACK: return "Format not supported for playback";
      case TRANSPORT_LOCKED: return "Transport locked";
      case WRITE_ERROR: return "Write error";
      case MEDIA_NOT_WRITABLE: return "Media not writeable";
      case FORMAT_NOT_SUPPORTED_FOR_RECORDING: return "Format not supported for recording";
      case MEDIA_FULL: return "Media full";
      case SEEK_MODE_NOT_SUPPORTED: return "Seek mode not supported";
      case ILLEGAL_SEEK_TARGET: return "Illegal seek target";
      case BAD_METADATA: return "Bad metadata";
      case RECORD_QUALITY_NOT_SUPPORTED: return "Record quality not supported";
      case ILLEGAL_MIME_TYPE: return "Illegal MIME type";
      case CONTENT_BUSY: return "Content busy";
      case RESOURCE_NOT_FOUND: return "Resource not found";
      case PLAY_SPEED_NOT_SUPPORTED: return "Play speed not supported";
    }
    
    return UPnPStatus.code2String(code);
  }
  
}

